package zdream.control.world;

import zdream.control.assets.FoeDescription;
import zdream.control.assets.FoeStorage;
import zdream.control.graphics.AbstractLevelGraphics;
import zdream.control.input.WorldInput;
import zdream.control.profile.IProfile;
import zdream.control.region.FieldSpec;
import zdream.control.region.FoeSpawnPoint;
import zdream.control.region.Gate;
import zdream.control.region.InterestPoint;
import zdream.control.region.ItemSpec;
import zdream.control.region.Region;
import zdream.control.region.Room;
import zdream.control.region.SavePoint;
import zdream.utils.function.VoidFunction;
import zdream.utils.math.Rectangle;

import java.lang.reflect.Constructor;
import java.util.*;
import java.util.function.BooleanSupplier;

/**
 * <p>负责管理所有的 Foe, Box, 房间切换
 * </p>
 *
 * @author Zdream
 * @date 2021-09-07 (last created)
 * @date 2022-05-28 (last modified)
 */
public class LevelWorld {
	public LevelWorld() {
		this(new LevelWorldSpec());
	}

	public LevelWorld(LevelWorldSpec spec) {
		this.roomTransmission = new RoomTransmission(this);
		this.camera = new LevelCamera(this);
		this.resourcePath = spec.resourcePath;
		this.graphics = spec.graphics;
		this.global = new FakeFoe();

		initFoeStorage(spec);
	}

	public static class LevelWorldSpec {
		public String resourcePath;
		public AbstractLevelGraphics graphics;
		public FoeStorage storage;
	}

	/**
	 * 处理 room 切换的
	 */
	public final RoomTransmission roomTransmission;
	public final LevelCamera camera;

	/* **********
	 * 事件发布 *
	 ********** */

	class FakeFoe extends MFoe {
		public FakeFoe() {
			super(LevelWorld.this);
		}
		@Override
		public String foeName() {
			return "GlobalWorld";
		}

		@Override
		public void init() {
			onHook(MHookType.RECEIVE_DAMAGE)
					.withPriority(-9999)
					.handle(MHooks.MHookContext::abort)
					.buildAndSave();
			onHook(MHookType.RECOVER)
					.withPriority(-9999)
					.handle(MHooks.MHookContext::abort)
					.buildAndSave();
		}
	}

	/**
	 * {@link LevelWorld} 的事件发布也使用 {@link MHooks} 体系.
	 * 它采用一个虚假的 {@link MFoe} 来记录全局事件.
	 *
	 * global 也是一个缺省的 foe, 比如由刺地形触发的伤害, 攻击者就是 global.
	 */
	public final FakeFoe global;

	public MHooks.MHookContext triggerEvent(MHookType type) {
		System.out.println("全局事件: " + type);
		return global.triggerHook(type);
	}

	public BooleanSupplier putSubscriber(VoidFunction listener, MHookType type) {
		return global.onHook(type)
				.handle(listener)
				.buildAndSave();
	}

	public MHookStore.HookHandlerBuilder createSubscriber(MHookType type) {
		return global.onHook(type);
	}

	/* **********
	 *   行动   *
	 ********** */
	public final Ticker ticker = new Ticker();

	{
		ticker.everyTick = this::tickByState;
		ticker.afterFrame = this::tickGraphics;
	}

	public static final byte STATE_NONE = 0;
	public static final byte STATE_READY = 1;
	public static final byte STATE_PLAYING = 2;
	public static final byte STATE_OVER = 3;
	public byte state;

	public long lastUpdateTime; // use world time

	public static final long PLAYER_ENTER_DURATION = 1200;
	public static final long WORLD_CLOSE_DURATION = 3000;
	public long
			startForeplayMillis = PLAYER_ENTER_DURATION,
			endForeplayMillis = WORLD_CLOSE_DURATION;

	public void render(float delta) {
		ticker.runFrame(delta);
	}

	protected void tickByState() {
		switch (state) {
		case STATE_NONE:
			resourceCheck();
			tickByState();
			break;
		case STATE_READY:
			tick();
			ready();
			break;
		case STATE_PLAYING:
			tick();
			checkIsOver();
			break;
		case STATE_OVER:
			tick();
			over();
			break;
		}
	}

	private void resourceCheck() {
		this.state = STATE_READY;

		if (profile1 == null) {
			System.out.println("因为 profile1 未设置, LevelWorld 将跳过 ready 阶段");
			this.state = STATE_PLAYING;
		}
		if (player1 != null) {
			throw new IllegalStateException("player1 未清理");
		}
		lastUpdateTime = ticker.worldTime;
	}

	private void ready() {
		if (profile1 == null) {
			throw new IllegalStateException("在 profile1 未设置的情况下, LevelWorld 不能进入 ready 阶段");
		}
		long duration = ticker.worldTime - lastUpdateTime;
		if (duration >= startForeplayMillis) {
			convertToPlayingState();
		}
	}

	private void checkIsOver() {
		if (profile1 == null) {
			return;
		}
		if (!profile1.alive()) {
			this.state = STATE_OVER;
			lastUpdateTime = ticker.worldTime;
		}
	}

	private void over() {
		long duration = ticker.worldTime - lastUpdateTime;
		if (duration < endForeplayMillis) {
			return;
		}

		this.clearAllFoes();
		this.applyWorld(this.currentSavePoint);
		this.state = STATE_READY;
		this.lastUpdateTime = ticker.worldTime;
		this.player1 = null;
		this.profile1.reset();
	}

	private void convertToPlayingState() {
		this.state = STATE_PLAYING;
		this.player1 = profile1.createFoe(this);
		createFoe(player1,
				currentSavePoint.xInRoom,
				currentSavePoint.yInRoom,
				currentSavePoint.orientation);
		this.focusFoe = player1;
		final WorldInput input = profile1.getInput();
		if (input != null) {
			player1.putInput(input);
		}
		camera.focusToFoeOnly(player1);
	}

	/**
	 * 调用了 pause 之后, {@link #render(float)} 方法就不会再让世界运行了
	 */
	public void pause() {
		ticker.paused = true;
	}

	public void resume() {
		ticker.paused = false;
	}

	/* **********
	 *  Ticker  *
	 ********** */

	private void tick() {
		tickFoes();
		cleanDestroyedFoes();
		checkTransmission();
		camera.tick(ticker);
		if (profile1 != null) {
			profile1.refresh();
		}
	}

	private void tickFoes() {
		if (tmpFoes == null) {
			buildTmpFoes();
		}

		for (MFoe foe : tmpFoes) {
			if (foe.isDestroyed()) {
				continue;
			}
			foe.tick(ticker);
		}

		for (MFoe texture : textures) {
			texture.tick(ticker);
		}
	}

	private void cleanDestroyedFoes() {
		boolean anyDeleted = foes.removeIf(MFoe::isDestroyed);
		if (anyDeleted) {
			tmpFoes = null;
		}
		textures.removeIf(MFoe::isDestroyed);
	}

	private void checkTransmission() {
		if (inRoomTransmission) {
			roomTransmission.tickTransmission(ticker);
			return;
		}

		if (focusFoe == null || currentRoom.gates == null) {
			return;
		}

		Gate selected = null;
		for (Gate gate : currentRoom.gates) {
			boolean matched = roomTransmission.checkShift(focusFoe, gate);
			if (matched) {
				selected = gate;
				break;
			}
		}

		if (selected == null) {
			return;
		}

		ArrayList<MFoe> retainFoes = new ArrayList<>();
		retainFoes.add(focusFoe);
		roomTransmission.startTransmission(selected, retainFoes);
	}

	/* **********
	 *  Region  *
	 ********** */
	public Room currentRoom;
	public boolean inRoomTransmission;

	public void setCurrentRoom(Room currentRoom) {
		this.currentRoom = Objects.requireNonNull(currentRoom);
		for (FieldSpec field : currentRoom.fields) {
			createFoe(field);
		}
		for (FoeSpawnPoint point : currentRoom.foes) {
			createFoe(point);
		}

		triggerEvent(MHookType.ROOM_UPDATED).run();
	}

	/* **********
	 *   Foes   *
	 ********** */
	private MFoe[] tmpFoes;
	public final List<MFoe> foes = new ArrayList<>();

	/**
	 * 不参与世界中交互的 foe, 大多数是类型 {@link MFoe#TYPE_TEXTURE}
	 */
	public final List<MFoe> textures = new ArrayList<>();

	/**
	 * 设置关注的 foe, 通过这些 foes 来判断是否需要房间切换.
	 * 一般和 camera 中的 focus 相同, 在强制版面时不一样.
	 */
	public MFoe focusFoe;

	public IProfile profile1;
	public MFoe player1;

	private void buildTmpFoes() {
		tmpFoes = foes.toArray(new MFoe[0]);
	}

	public void clearAllFoes() {
		// 清理原来房间的 foes
		for (MFoe foe : foes) {
			if (!foe.isDestroyed()) {
				foe.destroy();
			}
		}
		for (MFoe foe : textures) {
			if (!foe.isDestroyed()) {
				foe.destroy();
			}
		}
		foes.clear();
		textures.clear();
	}

	public MFoe findFoe(int id) {
		for (MFoe foe : foes) {
			if (foe.id == id) {
				return foe;
			}
		}
		for (MFoe foe : textures) {
			if (foe.id == id) {
				return foe;
			}
		}
		return null;
	}

	private void addFoe(MFoe foe) {
		if (foe.is(MFoe.TYPE_TEXTURE)) {
			textures.add(foe);
			return;
		}

		this.foes.add(foe);
		this.tmpFoes = null;
	}

	public void setPlayer1Profile(IProfile profile) {
		profile1 = profile;
	}

	/* **********
	 *  Region  *
	 ********** */

	public SavePoint currentSavePoint;

	public void applyWorld(Region region) {
		if (this.graphics != null) {
			this.graphics.descriptionFinder = storage::findByFoeName;
		}
		final SavePoint point = Arrays.stream(region.points)
				.filter(InterestPoint::isForSave)
				.map(InterestPoint::asSave)
				.filter(p -> p.startPoint)
				.findAny()
				.orElse(null);
		if (point == null) {
			throw new IllegalArgumentException("Region: " + region.name + " 没有启动点");
		}
		applyWorld(point);
	}

	public void applyWorld(SavePoint point) {
		currentSavePoint = Objects.requireNonNull(point);
		setCurrentRoom(point.room);
		camera.setCenterPointAndFix(currentSavePoint.xInRoom, currentSavePoint.yInRoom);
	}

	/* *************
	 * Foe Builder *
	 ************* */
	public FoeStorage storage;
	public final String resourcePath;

	void initFoeStorage(LevelWorldSpec spec) {
		if (spec.storage != null) {
			storage = spec.storage;
			return;
		}

		storage = new FoeStorage();
		if (resourcePath == null) {
			storage.scan();
		} else {
			storage.scan(resourcePath);
		}
	}

	public MFoe createFoe(FieldSpec def) {
		return createFoe(def.spec.clone().withFieldRectangle(def.rectangle));
	}

	public MFoe createFoe(FoeSpawnPoint def) {
		return createFoe(def.spec.clone().withPosition(def.xInRoom, def.yInRoom));
	}

	public MFoe createFoe(String foeName, float x, float y, boolean orientation) {
		return createFoe(ItemSpec.of(foeName, x, y, orientation));
	}

//	public MFoe createFoe(ItemSpec spec) {
//		final MFoe foe = createFoe(spec.name);
//		addFoe(foe);
//		foe.init(spec);
//		afterFoeInitialization(foe);
//		return foe;
//	}

	public MFoe createFoe(MFoe foe, float x, float y, boolean orientation) {
		addFoe(foe);
		foe.prepare(ItemSpec.of(foe.foeName(), x, y, orientation));
		foe.init();
		afterFoeInitialization(foe);
		return foe;
	}

	private void afterFoeInitialization(MFoe foe) {
		if (foe.description == null) {
			FoeDescription description = storage.findByFoeName(foe.foeName());
			if (description == null) {
				System.out.printf("未找到 %s 对应的 FoeDescription: 你是否忘记写它的 json 文件了?", foe);
			} else {
				foe.description = description;
			}
		}

		foe.flush();
		final MHooks.FoeHookContext context = global.triggerHook(MHookType.FOE_ADDED).as();
		context.withFoe(foe).run();

		if (foe.not(MFoe.TYPE_FIELD) && foe.not(MFoe.TYPE_TEXTURE)) {
			foe.terrainHandler = foe.terrainHandler == null ? defaultHandler : foe.terrainHandler;
			foe.terrainHandler.freshBox(foe);
		}
	}

	/**
	 * <p>创建 Foe 实例 (未初始化的)
	 * <p>创建的步骤如下:
	 * <li>按照 json 配置的 class 生成人物类实例 (如果类不是 CharacterEntry 的派生类则不会被生成)
	 * <li>按照 json 配置的 modules 说明逐个创建模块并加入到人物实例中
	 * <li>调用人物类实例的 init 方法, 将 json 文件作为参数传入
	 * </li>
	 * </p>
	 * @param spec
	 *   Foe 初始化参数. 注意这个 spec 要修改内容, 所以最好传入 {@link ItemSpec#clone()} 的结果
	 * @return Foe
	 * @throws NullPointerException
	 *   当 name 对应的人物数据不存在时
	 */
	@SuppressWarnings("unchecked")
	private MFoe createFoe(ItemSpec spec) {
		String name = spec.name;
		FoeDescription description = storage.findByFoeName(name);
		if (description == null) {
			throw new NullPointerException(String.format("不存在 %s 的 Foe 数据", name));
		}

		Class<?> foeClass;
		try {
			foeClass = Class.forName(description.className);
		} catch (ClassNotFoundException e) {
			throw new IllegalArgumentException(String.format("Foe class: %s 不存在", description.className), e);
		}

		boolean assignableFromFoe = MFoe.class.isAssignableFrom(foeClass);
		if (!assignableFromFoe) {
			throw new IllegalArgumentException(String.format("class: %s 不是 Foe 的子类", description.className));
		}

		Class<? extends MFoe> castClass = (Class<? extends MFoe>) foeClass;

		final MFoe foe;
		try {
			Constructor<? extends MFoe> constructors = castClass.getConstructor(LevelWorld.class);
			foe = constructors.newInstance(this);
		} catch (Exception e) {
			throw new IllegalStateException(String.format("%s 的角色数据中, 初始化类创建失败", name), e);
		}

		foe.description = description;
		addFoe(foe);

		if (description.spec == null) {
			foe.prepare(spec);
		} else {
			foe.prepare(description.spec, spec);
		}

		foe.init();
		afterFoeInitialization(foe);
		return foe;
	}

	/* ************
	 * 地形和位置 *
	 ************ */
	public final TerrainHandler defaultHandler = new TerrainHandler(this);
	private final Rectangle tmp = new Rectangle();

	public boolean isOutsideRoom(MFoe foe) {
		Rectangle rect = foe.flush().pos;
		return 0 > rect.x + rect.width
				|| currentRoom.width < rect.x
				|| 0 > rect.y + rect.height
				|| currentRoom.height < rect.y;
	}

	public boolean isOutsideCamera(MFoe foe) {
		// TODO
		return isOutsideRoom(foe);
	}

	public boolean dropOutside(MFoe foe) {
		foe.flush();
		if (foe.gravityDown) {
			return foe.pos.y + foe.boxHeight < 0;
		} else {
			return foe.pos.y > currentRoom.height;
		}
	}

	public void findOverlapFoeAndPutInList(List<MFoe> candidates, float x, float y, float width, float height) {
		tmp.set(x, y, width, height);
		findOverlapFoeAndPutInList(candidates, tmp);
	}

	public void findOverlapFoeAndPutInList(List<MFoe> candidates, Rectangle box) {
		if (tmpFoes == null) {
			buildTmpFoes();
		}

		for (MFoe tmpFoe : tmpFoes) {
			if (tmpFoe.isDestroyed()) {
				continue;
			}
			if (tmpFoe.flush().pos.overlaps(box)) {
				candidates.add(tmpFoe);
			}
		}
	}

	/**
	 * 是否站在上面, 站在上面返回 true. 以 topBox 的重力方向为准. 该方法没判断盒子地形.
	 * @param topBox 作为站立方
	 * @param groundBox 作为被站立的平台
	 */
	public boolean stand(Box topBox, Box groundBox) {
		if (topBox.gravityScale <= 0) {
			return false;
		}
		if (topBox.gravityDown) {
			if (topBox.lastAnchorY < topBox.anchorY && topBox.velocityY < 0) return false;
			topBox.flush();
			groundBox.flush();
			if (topBox.pos.y != groundBox.pos.y + groundBox.pos.height
					|| topBox.pos.x >= groundBox.pos.x + groundBox.pos.width
					|| topBox.pos.x + topBox.pos.width <= groundBox.pos.x) return false;
		} else {
			if (topBox.lastAnchorY > topBox.anchorY && topBox.velocityY > 0) return false;
			topBox.flush();
			groundBox.flush();
			if (topBox.pos.y + groundBox.pos.height != groundBox.pos.y
					|| topBox.pos.x >= groundBox.pos.x + groundBox.pos.width
					|| topBox.pos.x + topBox.pos.width <= groundBox.pos.x) return false;
		}
		return true;
	}

	/* **********
	 *   绘制   *
	 ********** */
	public AbstractLevelGraphics graphics;

	protected void tickGraphics() {
		if (graphics == null) {
			return;
		}
		graphics.draw(ticker);
	}
}
